Completed
Push — master ( b3f358...57140e )
by Jan
14s queued 12s
created

Frame.ts ➔ decompressBuffer   B

Complexity

Conditions 6

Size

Total Lines 32
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 20
dl 0
loc 32
rs 8.4666
c 0
b 0
f 0
cc 6
1
import zlib = require('zlib')
2
import {
3
    Flags,
4
    getHeaderSize,
5
    FrameHeader
6
} from './FrameHeader'
7
import * as GenericFrames from './frames/generic'
8
import { Frames } from './frames/frames'
9
import * as ID3Util from './ID3Util'
10
import { deduplicate, isBuffer, isKeyOf } from "./util"
11
12
type HeaderInfo = {
13
    identifier: string
14
    headerSize: number
15
    bodySize: number
16
    flags: Flags
17
}
18
19
export class Frame {
20
    identifier: string
21
    private value: unknown
22
    flags: Flags
23
24
    constructor(identifier: string, value: unknown, flags: Flags = {}) {
25
        this.identifier = identifier
26
        this.value = value
27
        this.flags = flags
28
    }
29
30
    static createFromBuffer = createFromBuffer
31
32
    getValue() {
33
        return this.value
34
    }
35
}
36
37
function createFromBuffer(
38
    frameBuffer: Buffer,
39
    version: number
40
): Frame | null {
41
    const headerSize = getHeaderSize(version)
42
    // Specification requirement
43
    if (frameBuffer.length < headerSize + 1) {
44
        return null
45
    }
46
    const headerBuffer = frameBuffer.subarray(0, headerSize)
47
    const header: HeaderInfo = {
48
        headerSize,
49
        ...FrameHeader.createFromBuffer(headerBuffer, version)
50
    }
51
    if (header.flags.encryption) {
52
        return null
53
    }
54
55
    const body = decompressBody(
56
        header.flags,
57
        getDataLength(header, frameBuffer),
58
        getBody(header, frameBuffer)
59
    )
60
    if (body) {
61
        const value = makeFrameValue(header.identifier, body, version)
62
        if (value) {
63
            return new Frame(header.identifier, value, header.flags)
64
        }
65
    }
66
    return null
67
}
68
69
export function makeFrameBuffer(identifier: string, value: unknown) {
70
    if (isKeyOf(identifier, Frames)) {
71
        return handleMultipleAndMakeFrameBuffer(
72
            identifier,
73
            value,
74
            Frames[identifier].create
75
        )
76
    }
77
    if (identifier.startsWith('T')) {
78
        return GenericFrames.GENERIC_TEXT.create(identifier, value)
79
    }
80
    if (identifier.startsWith('W')) {
81
        return handleMultipleAndMakeFrameBuffer(
82
            identifier,
83
            value,
84
            url => GenericFrames.GENERIC_URL.create(identifier, url),
85
            deduplicate
86
        )
87
    }
88
    return null
89
}
90
91
function handleMultipleAndMakeFrameBuffer<
92
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
93
    Create extends (value: any, index: number) => Buffer | null
94
>(
95
    identifier: string,
96
    data: unknown,
97
    create: Create,
98
    deduplicate = (values: ([Parameters<Create>])[]) => values
99
) {
100
    const values = makeValueArray(identifier, data)
101
    const frames = deduplicate(values)
102
        .map(create)
103
        .filter(isBuffer)
104
    return frames.length ? Buffer.concat(frames) : null
105
}
106
107
/**
108
 * Throws if an array is given but not expected, i.e. the contract is not
109
 * respected, otherwise always return an array.
110
 */
111
function makeValueArray(identifier: string, data: unknown) {
112
    const isMultiple = ID3Util.getSpecOptions(identifier).multiple
113
    const isArray = Array.isArray(data)
114
    if (!isMultiple && isArray) {
115
        throw new TypeError(`Unexpected array for frame ${identifier}`)
116
    }
117
    return isMultiple && isArray ? data : [data]
118
}
119
120
function makeFrameValue(identifier:string, body: Buffer, version: number) {
121
    if (isKeyOf(identifier, Frames)) {
122
        return Frames[identifier].read(body, version)
123
    }
124
    if (identifier.startsWith('T')) {
125
        return GenericFrames.GENERIC_TEXT.read(body)
126
    }
127
    if (identifier.startsWith('W')) {
128
        return GenericFrames.GENERIC_URL.read(body)
129
    }
130
    return null
131
}
132
133
function getBody({flags, headerSize, bodySize}: HeaderInfo, buffer: Buffer) {
134
    const bodyOffset = flags.dataLengthIndicator ? 4 : 0
135
    const bodyStart = headerSize + bodyOffset
136
    const bodyEnd = bodyStart + bodySize - bodyOffset
137
    const body = buffer.subarray(bodyStart, bodyEnd)
138
    if (flags.unsynchronisation) {
139
        // This method should stay in ID3Util for now because it's also used
140
        // in the Tag's header which we don't have a class for.
141
        return ID3Util.processUnsynchronisedBuffer(body)
142
    }
143
    return body
144
}
145
146
function getDataLength({flags, headerSize}: HeaderInfo, buffer: Buffer) {
147
    return flags.dataLengthIndicator ? buffer.readInt32BE(headerSize) : 0
148
}
149
150
function decompressBody(
151
    {compression}: Flags,
152
    dataLength: number,
153
    body: Buffer
154
) {
155
    return compression ? decompressBuffer(body, dataLength) : body
156
}
157
158
function decompressBuffer(buffer: Buffer, expectedDecompressedLength: number) {
159
    if (buffer.length < 5) {
160
        return null
161
    }
162
163
    // ID3 spec defines that compression is stored in ZLIB format,
164
    // but doesn't specify if header is present or not.
165
    // ZLIB has a 2-byte header.
166
    // 1. try if header + body decompression
167
    // 2. else try if header is not stored (assume that all content is deflated "body")
168
    // 3. else try if inflation works if the header is omitted (implementation dependent)
169
    const tryDecompress = () => {
170
        try {
171
            return zlib.inflateSync(buffer)
172
        } catch (error) {
173
            try {
174
                return zlib.inflateRawSync(buffer)
175
            } catch (error) {
176
                try {
177
                    return zlib.inflateRawSync(buffer.subarray(2))
178
                } catch (error) {
179
                    return null
180
                }
181
            }
182
        }
183
    }
184
    const decompressed = tryDecompress()
185
    if (decompressed && decompressed.length === expectedDecompressedLength) {
186
        return decompressed
187
    }
188
    return null
189
}
190